Skip to content

feat(github): manage GitHub org declaratively via Crossplane provider-upjet-github#2039

Merged
devantler merged 10 commits into
mainfrom
claude/github-crossplane
Jun 18, 2026
Merged

feat(github): manage GitHub org declaratively via Crossplane provider-upjet-github#2039
devantler merged 10 commits into
mainfrom
claude/github-crossplane

Conversation

@devantler

@devantler devantler commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

What

Brings the devantler-tech GitHub org under GitOps: GitHub repositories (and over time rulesets, labels, branch protection, …) become Crossplane managed resources reconciled against the GitHub API by provider-upjet-github — no more GitHub UI fiddling; out-of-band UI edits get reverted.

The org's desired state lives in devantler-tech/.github under deploy/ (merged, published as the cosign-signed github-config OCI artifact) and is onboarded here as a standard app (github-config) — the same pattern as every other tenant. No bespoke Flux Kustomization.

Architecture (standard tiers — controller → its CRs → app)

Piece Where
Crossplane core (HelmRelease, prod-only) k8s/providers/hetzner/infrastructure/controllers/crossplane/
Provider package + activation policy (pkg.crossplane.io CRs, one tier after the controller — like Coroot/Flagger) k8s/providers/hetzner/infrastructure/crossplane/
The github-config app (ns, SA, least-priv Role, ESO creds, namespaced ProviderConfig, seed PushSecret, cosign-verified OCIRepository+Kustomization) — consumed by the existing apps Flux Kustomization k8s/bases/apps/github-config/
Desired state (managed resources) devantler-tech/.github deploy/
OpenBao policy/role wiring k8s/bases/infrastructure/vault-config/job.yaml
Docs docs/github-management.md

Key decisions:

  • Standard app pattern, not a dedicated kustomization — onboarded via the existing apps layer like wedding-app/ascoachingogvaner; the earlier bespoke github Flux Kustomization was removed (it diverged from the proven pattern).
  • Namespaced managed resources (Crossplane v2, repo.github.m.upbound.io) — appliable by a namespace-scoped app SA with a plain Role (no ClusterRoleBinding); the Role is not aggregated into edit, so no other tenant inherits GitHub powers.
  • Seed PushSecret lives with the app (leaf apps layer) so a missing-credential stall can never block the infrastructure→apps chain.
  • provider.defaultActivations: [] + explicit ManagedResourceActivationPolicy — only the namespaced MRDs we use become CRDs (~3MiB apiserver each).
  • Safety rails: Observe-first adoption; namespaced MRs omit Delete from managementPolicies so neither CR deletion nor a Flux prune can delete a real repo.
  • New dependency flagged: Crossplane core + provider-upjet-github (community, v0.x; wraps integrations/terraform-provider-github).

Maintainer follow-up (one-time, before/with rollout)

Create the org GitHub App (Repository Administration R/W, Contents R, Organization Administration R/W; install on all repos; no webhook) and add github_app_id / github_app_installation_id / github_app_pem to k8s/clusters/prod/bootstrap/variables-cluster-secret.enc.yaml. Seed these before rollout so the app reconciles green immediately (the seed PushSecret otherwise sits red in the leaf apps layer — isolated, non-blocking).

Validation

ksail workload validate (local, 337 files ✔) and ksail --config ksail.prod.yaml workload validate (all github-config + crossplane groups ✔). github-config is correctly excluded from the local/docker cluster (prod-only).

🤖 Generated with Claude Code

…-upjet-github

GitHub repos/rulesets/labels become Kubernetes custom resources reconciled
by provider-upjet-github, replacing manual GitHub UI administration.

- crossplane core HelmRelease (prod-only controllers overlay, chart 2.3.2)
  with provider.defaultActivations=[] so only explicitly activated MRDs
  become CRDs (~3MiB apiserver each; the provider ships 77)
- dedicated `github` Flux Kustomization (dependsOn: infrastructure) so a
  GitHub-side stall never holds the infrastructure→apps deploy chain red
- GitHub App auth: PushSecret seeds OpenBao from variables-cluster (SOPS),
  ExternalSecret renders the provider's app_auth JSON credential
- Observe-first adoption: first two Repository MRs (platform, ksail) bound
  by external-name with managementPolicies=[Observe], deletionPolicy=Orphan
- docs/github-management.md: architecture, credential setup, adoption flow

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…spaced MRs

Rework: instead of holding cluster-scoped Repository CRs in the platform
tree, the org's desired GitHub state lives in devantler-tech/.github under
deploy/ and is onboarded as an isolated tenant.

- switch the activation policy + ProviderConfig + managed resources to the
  Crossplane v2 NAMESPACED variants (repo.github.m.upbound.io), so a
  namespace-scoped tenant SA with a plain Role can apply them — no cluster
  RBAC, no ClusterRoleBinding
- add the github-config tenant: namespace, least-privilege SA + Role (NOT
  aggregated into edit, so other tenants don't inherit GitHub powers),
  ghcr + GitHub App credential ExternalSecrets, namespaced ProviderConfig,
  and a cosign-verified OCIRepository + Kustomization that pull .github's
  artifact and apply it as the tenant SA
- engine (provider package + activation policy + credential seed) stays
  platform-side; desired-state managed resources move to .github/deploy/
- remove the cluster-scoped repositories/ CRs (now in .github)
- docs rewritten for the tenant + namespaced architecture

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
# Conflicts:
#	k8s/bases/infrastructure/vault-config/job.yaml
#	k8s/providers/hetzner/infrastructure/controllers/kustomization.yaml
…signer

The .github repo's cd.yaml now publishes the github-config OCI artifact by
calling the shared publish-manifests reusable workflow
(reusable-workflows#319) instead of signing inline. Cosign keyless signing
runs INSIDE that reusable workflow, so the certificate's OIDC subject is the
reusable workflow's path — not .github's own cd.yaml.

Update the github-config OCIRepository's verify.matchOIDCIdentity.subject to
match the new signer:
  ^…/devantler-tech/reusable-workflows/.github/workflows/publish-manifests.yaml@.+$
(same convention as the publish-app.yaml-signed app artifacts).

This must land together with .github#39 (the caller cutover) so the verified
identity always matches the signer. The github-config artifact is not yet in
production (both PRs are drafts), so there is no live verification to break.

Validated: `kubectl kustomize k8s/providers/hetzner/github/` builds clean
with the updated subject.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@devantler devantler added this pull request to the merge queue Jun 13, 2026
@github-merge-queue github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Jun 13, 2026
@devantler devantler added this pull request to the merge queue Jun 14, 2026
@github-merge-queue github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Jun 14, 2026
@devantler devantler added this pull request to the merge queue Jun 15, 2026
@github-merge-queue github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Jun 15, 2026
@devantler devantler added this pull request to the merge queue Jun 15, 2026
@github-merge-queue github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Jun 15, 2026
@botantler-1 botantler-1 Bot enabled auto-merge June 15, 2026 18:44
@botantler-1 botantler-1 Bot added this pull request to the merge queue Jun 15, 2026
@github-merge-queue github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Jun 15, 2026
@devantler devantler added this pull request to the merge queue Jun 16, 2026
@devantler devantler removed this pull request from the merge queue due to a manual request Jun 16, 2026
…bespoke Flux Kustomization

The dedicated `github` Flux Kustomization (clusters/prod/github-flux-kustomization.yaml
+ providers/hetzner/github/) diverged from the proven pattern. Rework to the
standard tiers:

- Crossplane core HelmRelease stays in infrastructure/controllers/crossplane/
- Provider package + ManagedResourceActivationPolicy move to the infrastructure
  layer (providers/hetzner/infrastructure/crossplane/), one tier after the
  controller — same controller-then-CR ordering as Coroot/Flagger
- the GitHub state is onboarded as a normal app, bases/apps/github-config/
  (namespace, SA, least-priv Role, ESO creds, namespaced ProviderConfig, seed
  PushSecret, cosign-verified OCIRepository+Kustomization), consumed by the
  existing `apps` Flux Kustomization — no bespoke kustomization
- the credential seed PushSecret lives with the app (leaf apps layer) so a
  missing-key stall can never block the infrastructure→apps chain
- delete the bespoke github layer + github-flux-kustomization.yaml; update
  docs/github-management.md + AGENTS.md

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@botantler-1 botantler-1 Bot enabled auto-merge June 16, 2026 07:47
@botantler-1 botantler-1 Bot added this pull request to the merge queue Jun 16, 2026
@github-merge-queue github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Jun 16, 2026
@devantler devantler marked this pull request as draft June 16, 2026 21:04
… SOPS PushSecret

Replace the variables-cluster (SOPS) PushSecret seed with a declarative,
idempotent placeholder seed in the vault-config bootstrap Job: it writes
REPLACE_WITH_* values to secret/infrastructure/github/app only when the secret
is absent, so the maintainer overwrites them in place via the OpenBao UI/CLI and
a later Job re-run never clobbers the real values. OpenBao is backed up (Raft
snapshots → R2), so the manually-set values are durable without a GitOps source.

- add the only-if-absent kv seed to vault-config/job.yaml (section 1b)
- remove seed-github-app.yaml PushSecret + its kustomization entry
- drop the now-unused vault-seed-write github paths (keep infra-github-readonly,
  which the github-app-credentials ExternalSecret still needs to read)
- docs: credential setup is now 'overwrite the OpenBao placeholders', no SOPS

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@devantler devantler marked this pull request as ready for review June 17, 2026 19:54
…hcr-auth

Tighten the github-config tenant's privileges and move tenant-specific manifests
out of the platform:

- add a namespaced SecretStore (k8s/bases/apps/github-config/secretstore.yaml)
  backed by a dedicated 'github-config' OpenBao role scoped to
  infrastructure/github/* only (mirrors the wedding-app pattern); the tenant no
  longer uses the broad cluster-scoped openbao ClusterSecretStore
- remove infra-github-readonly from the shared external-secrets role bundle (now
  carried solely by the dedicated github-config role)
- move the github-app-credentials ExternalSecret + the namespaced ProviderConfig
  to devantler-tech/.github deploy/ (tenant-applied); the app SA Role gains verbs
  on external-secrets.io + github.m.upbound.io accordingly
- drop ghcr-auth entirely: the github-config/manifests package is public, so the
  OCIRepository pulls anonymously (no secretRef, no ExternalSecret, no SA
  imagePullSecrets)
- docs updated

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@botantler-1 botantler-1 Bot enabled auto-merge June 17, 2026 20:03
@devantler devantler disabled auto-merge June 17, 2026 20:07
@devantler devantler enabled auto-merge June 17, 2026 20:07
@devantler devantler added this pull request to the merge queue Jun 17, 2026
@github-merge-queue github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Jun 17, 2026
@devantler devantler merged commit ee345ad into main Jun 18, 2026
10 checks passed
@github-project-automation github-project-automation Bot moved this from 🫴 Ready to ✅ Done in 🌊 Project Board Jun 18, 2026
@devantler devantler deleted the claude/github-crossplane branch June 18, 2026 07:52
@botantler-1

botantler-1 Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 1.64.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

Status: ✅ Done

Development

Successfully merging this pull request may close these issues.

1 participant